package main

import (
	"os"
	"testing"
)

// TestNewlineProcedure tests the newline procedure
func TestNewlineProcedure(t *testing.T) {
	env := NewGlobalEnvironment()

	// Capture stdout for testing
	oldStdout := os.Stdout
	r, w, _ := os.Pipe()
	os.Stdout = w

	defer func() {
		w.Close()
		os.Stdout = oldStdout
	}()

	// Test newline with no arguments
	result, err := EvalString("(newline)", env)
	if err != nil {
		t.Errorf("Error evaluating (newline): %v", err)
		return
	}

	// Check that newline returns nil (or unspecified value)
	if result == nil || result.Type != TypeNil {
		t.Errorf("newline returned unexpected value: %v", result)
	}

	// Close the write end and restore stdout
	w.Close()
	os.Stdout = oldStdout

	// Read the captured output
	data := make([]byte, 1024)
	n, _ := r.Read(data)
	output := string(data[:n])

	// Check that a newline was written
	if output != "\n" {
		t.Errorf("Expected newline character '\\n', got %q", output)
	}

	// Test newline with too many arguments
	_, err = EvalString("(newline 1)", env)
	if err == nil {
		t.Error("Expected error for newline with arguments, but got none")
	}
}

// TestReadCharProcedure tests the read-char procedure with mocked input
func TestReadCharProcedure(t *testing.T) {
	env := NewGlobalEnvironment()

	// Mock stdin with a known input string using a pipe
	oldStdin := os.Stdin
	stdinReader, stdinWriter, _ := os.Pipe()
	os.Stdin = stdinReader

	// Write test input to the pipe
	go func() {
		defer stdinWriter.Close()
		stdinWriter.WriteString("a")
	}()

	defer func() {
		os.Stdin = oldStdin
	}()

	// Test reading a character
	result, err := EvalString("(read-char)", env)
	if err != nil {
		t.Errorf("Error evaluating (read-char): %v", err)
		return
	}

	// Check that the result is a character 'a'
	if result.Type != TypeChar || result.Char != 'a' {
		t.Errorf("Expected character 'a', got %v", result)
	}

	// Test read-char with too many arguments
	_, err = EvalString("(read-char 1)", env)
	if err == nil {
		t.Error("Expected error for read-char with arguments, but got none")
	}

	// Note: Testing EOF would require more complex mocking
}

// TestWriteCharProcedure tests the write-char procedure
func TestWriteCharProcedure(t *testing.T) {
	env := NewGlobalEnvironment()

	// Capture stdout for testing
	oldStdout := os.Stdout
	r, w, _ := os.Pipe()
	os.Stdout = w

	defer func() {
		w.Close()
		os.Stdout = oldStdout
	}()

	// Test writing a character
	result, err := EvalString("(write-char #\\a)", env)
	if err != nil {
		t.Errorf("Error evaluating (write-char #\\a): %v", err)
		return
	}

	// Check that write-char returns nil (or unspecified value)
	if result == nil || result.Type != TypeNil {
		t.Errorf("write-char returned unexpected value: %v", result)
	}

	// Close the write end and restore stdout
	w.Close()
	os.Stdout = oldStdout

	// Read the captured output
	data := make([]byte, 1024)
	n, _ := r.Read(data)
	output := string(data[:n])

	// Check that the character 'a' was written
	if output != "a" {
		t.Errorf("Expected character 'a', got %q", output)
	}

	// Test write-char with no arguments
	_, err = EvalString("(write-char)", env)
	if err == nil {
		t.Error("Expected error for write-char with no arguments, but got none")
	}

	// Test write-char with too many arguments
	_, err = EvalString("(write-char #\\a #\\b)", env)
	if err == nil {
		t.Error("Expected error for write-char with too many arguments, but got none")
	}

	// Test write-char with non-character argument
	_, err = EvalString("(write-char 42)", env)
	if err == nil {
		t.Error("Expected error for write-char with non-character argument, but got none")
	}
}

// TestEofObjectpProcedure tests the eof-object? procedure
func TestEofObjectpProcedure(t *testing.T) {
	env := NewGlobalEnvironment()

	// Mock stdin with empty input to simulate EOF using a pipe
	oldStdin := os.Stdin
	stdinReader, stdinWriter, _ := os.Pipe()
	os.Stdin = stdinReader

	// Close the writer immediately to simulate EOF
	stdinWriter.Close()

	defer func() {
		os.Stdin = oldStdin
	}()

	// We don't need to actually call read-char since we're using the internal eofObject directly
	// Just close the stdin pipe to prevent resource leaks

	// Define eof-object in the environment for testing
	// Since we can't directly print eof-object, we'll test with a different approach

	// Test that eof-object? correctly identifies the eof-object
	// We'll use the internal eofObject variable directly for this test
	result, err := builtinEofObjectp([]*Value{eofObject}, env)
	if err != nil {
		t.Errorf("Error evaluating builtinEofObjectp with eofObject: %v", err)
		return
	}

	if result.Type != TypeBoolean || !result.Boolean {
		t.Errorf("Expected #t for (eof-object? eofObject), got %v", result)
	}

	// Test that eof-object? returns #f for other values
	result, err = builtinEofObjectp([]*Value{NewBoolean(false)}, env)
	if err != nil {
		t.Errorf("Error evaluating builtinEofObjectp with #f: %v", err)
		return
	}

	if result.Type != TypeBoolean || result.Boolean {
		t.Errorf("Expected #f for (eof-object? #f), got %v", result)
	}

	// Test eof-object? with no arguments
	_, err = EvalString("(eof-object?)", env)
	if err == nil {
		t.Error("Expected error for eof-object? with no arguments, but got none")
	}

	// Test eof-object? with too many arguments
	_, err = EvalString("(eof-object? #f #t)", env)
	if err == nil {
		t.Error("Expected error for eof-object? with too many arguments, but got none")
	}
}

// TestAllIOProceduresIntegration tests the integration of all I/O procedures
func TestAllIOProceduresIntegration(t *testing.T) {
	env := NewGlobalEnvironment()

	// Mock stdin with test input using a pipe
	oldStdin := os.Stdin
	stdinReader, stdinWriter, _ := os.Pipe()
	os.Stdin = stdinReader

	// Write test input to the pipe
	go func() {
		defer stdinWriter.Close()
		stdinWriter.WriteString("hello\nworld")
	}()

	// Capture stdout for testing
	oldStdout := os.Stdout
	r, w, _ := os.Pipe()
	os.Stdout = w

	defer func() {
		w.Close()
		os.Stdout = oldStdout
		os.Stdin = oldStdin
	}()

	// Test a combination of I/O procedures
	// This reads "hello", writes it back, then writes a newline
	_, err := EvalString(`
	  (define c1 (read-char))
	  (define c2 (read-char))
	  (define c3 (read-char))
	  (define c4 (read-char))
	  (define c5 (read-char))
	  (write-char c1)
	  (write-char c2)
	  (write-char c3)
	  (write-char c4)
	  (write-char c5)
	  (newline)
	  #t
	`, env)
	if err != nil {
		t.Errorf("Error evaluating I/O integration test: %v", err)
		return
	}

	// Close the write end and restore stdout
	w.Close()
	os.Stdout = oldStdout

	// Read the captured output
	data := make([]byte, 1024)
	n, _ := r.Read(data)
	output := string(data[:n])

	// Check that the output is "hello\n"
	if output != "hello\n" {
		t.Errorf("Expected output 'hello\\n', got %q", output)
	}
}